package com.hydra.base.redis;

import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.*;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.NumberUtils;
import org.springframework.util.StringUtils;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * 参照 车务处理
 * Redis服务
 * @author：liangyongtong
 * @date：2020/1/7
 */
@SuppressWarnings("all")
@Configuration
@Component
public class RedisService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private TransmittableThreadLocal<String> lockFlag = new TransmittableThreadLocal<>();

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    private ValueOperations<String, String> valueOps;
    private ListOperations<String, String> listOps;
    private HashOperations<String, String, String> hashOps;
    private SetOperations<String, String> setOps;
    private ZSetOperations<String, String> zSetOps;
    private GeoOperations<String, String> geoOps;

    @PostConstruct
    public void init() {
        valueOps = redisTemplate.opsForValue();
        listOps = redisTemplate.opsForList();
        hashOps = redisTemplate.opsForHash();
        setOps = redisTemplate.opsForSet();
        zSetOps = redisTemplate.opsForZSet();
        geoOps = redisTemplate.opsForGeo();
    }

    /************************** 关于字符串的操作 **************************/
    /**
     * 根据key获取value
     *
     * @param key key
     * @return
     */
    public String get(String key) {
        return valueOps.get(key);
    }

    /**
     * 根据key获取value，并将value转成Long
     *
     * @param key key
     * @return
     */
    public Long getAsLong(String key) {
        String value = valueOps.get(key);
        return value == null ? null : NumberUtils.parseNumber(value, Long.class);
    }

    /**
     * 同时获得多个key的值
     * @param keys
     * @return
     */
    public List<String> multiGet(final Collection<String> keys) {
        return this.valueOps.multiGet(keys);
    }


    /**
     * 设置key和value
     *
     * @param key   key
     * @param value value
     */
    public void set(String key, String value) {
        valueOps.set(key, value);
    }

    /**
     * 如果key不存在,则返回true,否则返回false
     * @param key
     * @param value
     * @return
     */
    public Boolean setNx(String key,String value) {
        return valueOps.setIfAbsent(key,value);
    }


    /**
     * 设置key和value，带有过期时间
     *
     * @param key      key
     * @param value    value
     * @param timeout  过期时间
     * @param timeUnit 时间单位
     */
    public void set(String key, String value, long timeout, TimeUnit timeUnit) {
        valueOps.set(key, value, timeout, timeUnit);
    }

    /**
     * 如果key还不存在则进行设置，返回true，否则返回false.
     *
     * @param key   key
     * @param value value
     * @return
     */
    public Boolean setIfAbsent(String key, String value) {
        return valueOps.setIfAbsent(key, value);
    }

    /**
     * 如果key不存在则进行设置，返回true(同时设置过期时间)，否则返回false，
     *
     * @param key      key
     * @param value    value
     * @param timeout  过期时间
     * @param timeUnit 时间单位
     */
    public Boolean setIfAbsent(String key, String value,long timeout, TimeUnit timeUnit) {
        return valueOps.setIfAbsent(key, value, timeout, timeUnit);
    }

    /**
     * 同时设置多个key的值
     * @param keyValueMap
     */
    public void multiSet(Map<String,String> keyValueMap) {
        if(keyValueMap==null || keyValueMap.size()<=0) {
            return;
        }

        this.valueOps.multiSet(keyValueMap);
    }

    /**
     * 如果不能存的key进行设置
     * @param keyValueMap
     */
    public void multiSetIfAbsent(Map<String,String> keyValueMap) {
        if(keyValueMap==null || keyValueMap.size()<=0) {
            return;
        }

        this.valueOps.multiSetIfAbsent(keyValueMap);
    }

    /**
     * 设置新值，返回旧值
     *
     * @param key   key
     * @param value value
     * @return
     */
    public String getAndSet(String key, String value) {
        return valueOps.getAndSet(key, value);
    }

    /**
     * get指定key增加1
     *
     * @param key key
     * @return
     */
    public Long incr(String key) {
        return valueOps.increment(key, 1);
    }

    /**
     * 给指定key增加指定的值
     *
     * @param key       key
     * @param increment 增长的值
     * @return
     */
    public Long incrBy(String key, long increment) {
        return valueOps.increment(key, increment);
    }

    /**
     * get指定key减1
     * @param key
     * @return
     */
    public Long decr(String key) {
        return valueOps.decrement(key);
    }

    /**
     * 给指定key减指定的值
     *
     * @param key       key
     * @param decrement 减去的值
     * @return
     */
    public Long decr(String key, long decrement) {
        return valueOps.decrement(key, decrement);
    }

    /************************** 关于列表的操作 **************************/

    /**
     * 压栈操作,后压入的在栈顶
     *
     * @param key   key
     * @param value value
     */
    public void lpush(String key, String value) {
        listOps.leftPush(key, value);
    }

    /**
     * 弹出栈顶元素
     *
     * @param key key
     */
    public String lpop(String key) {
        return listOps.leftPop(key);
    }

    /**
     * 压栈操作,后压入的在栈顶,一次压入多个
     *
     * @param key    key
     * @param values values
     */
    public void lpushAll(String key, String... values) {
        listOps.leftPushAll(key, values);
    }

    /**
     * 在key对应list的尾部添加字符串元素,相当于加入队列
     *
     * @param key   key
     * @param value value
     */
    public void rpush(String key, String value) {
        listOps.rightPush(key, value);
    }

    /**
     * 取走列表最右侧的元素，相当于取走队列队尾的元素
     *
     * @param key key
     * @return
     */
    public String rpop(String key) {
        return listOps.rightPop(key);
    }

    /**
     * 在key对应list的尾部添加字符串元素,相当于加入队列,一次入多个
     *
     * @param key    key
     * @param values values
     */
    public void rpushAll(String key, String... values) {
        listOps.rightPushAll(key, values);
    }

    /**
     * 清空list
     *
     * @param key key
     */
    public void clearList(String key) {
        // 实际调用的是ltrim
        listOps.trim(key, 1, 0);
    }


    /**
     * 删除列表中 值=value 的所有元素
     * @param key
     * @param value
     */
    public void lDeleteElem(String key,String value) {
        listOps.remove(key,0,value);
    }

    /**
     * 获取列表所有元素
     *
     * @param key key
     * @return
     */
    public List<String> getListAllItems(String key) {
        // 实际调用lrange
        return listOps.range(key, 0, -1);
    }

    /**
     * 获取指定列表的长度
     *
     * @param key key
     * @return
     */
    public Long llen(String key) {
        return listOps.size(key);
    }

    /************************** 关于HASH的操作 **************************/

    /**
     * 获取hash接口中指定key的值
     *
     * @param key     key
     * @param hashKey hashKey
     * @return
     */
    public String hget(String key, String hashKey) {
        return hashOps.get(key, hashKey);
    }

    /**
     * 获取hash接口中指定key的值,并转换成Long类型
     *
     * @param key     key
     * @param hashKey hashKey
     * @return
     */
    public Long hgetAsLong(String key, String hashKey) {
        String hashValue = hashOps.get(key, hashKey);
        return hashValue == null ? null : NumberUtils.parseNumber(hashValue, Long.class);
    }

    /**
     * 给指定hash结构设置键值
     *
     * @param key       key
     * @param hashKey   hashKey
     * @param hashValue hashValue
     */
    public void hset(String key, String hashKey, String hashValue) {
        hashOps.put(key, hashKey, hashValue);
    }

    /**
     * 给指定hasp设置多组key/value
     *
     * @param key key
     * @param map key/valu
     */
    public void hsetAll(String key, Map<String, String> map) {
        hashOps.putAll(key, map);
    }

    /**
     * 获取指定hash结构的所有键值对
     *
     * @param key key
     * @return
     */
    public Map<String, String> hgetAll(String key) {
        return hashOps.entries(key);
    }

    /**
     * 判断是否拥有key
     *
     * @param key key
     * @return
     */
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 删除key
     *
     * @param key key
     */
    public void deleteKey(String key) {
        redisTemplate.delete(key);
    }


    /**
     * 删除key的集合
     *
     * @param keys 数组
     */
    public void deleteKeys(String... keys) {
        deleteKeys(Arrays.asList(keys));
    }


    /**
     * 删除key的集合
     *
     * @param keys key集合
     */
    public void deleteKeys(Collection<String> keys) {
        redisTemplate.delete(keys);
    }

    /**
     * 获取过期时间
     * @param key
     * @return
     */
    public Long getExpire(String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * 设置key的失效时间
     *
     * @param key      key
     * @param timeout  失效时间
     * @param timeUnit 时间单位
     */
    public void expire(String key, long timeout, TimeUnit timeUnit) {
        redisTemplate.expire(key, timeout, timeUnit);
    }

    /**
     * 设置key在某一时刻失效
     *
     * @param key  key
     * @param date 失效时刻
     */
    public void expireAt(String key, Date date) {
        redisTemplate.expireAt(key, date);
    }

    /**
     * 设置key在某一时刻失效
     *
     * @param key  key
     * @param 在 expireAt时刻失效
     */
    public void expireAt(String key, long expireAt) {
        expireAt(key, new Date(expireAt));
    }

    /**
     * 可以用于执行Redis脚本，如Lua脚本
     *
     * @return
     */
    public <T> T execute(RedisScript<T> redisScript, List<String> keys, Object... args) {
        return redisTemplate.execute(redisScript, keys, args);
    }

    /**
     * 直接用redis connnection执行操作
     * @param redisCallback
     */
    public <T> T executeInRedis(RedisCallback<T> redisCallback) {
        return redisTemplate.execute(redisCallback);
    }

    /********************* lock **********************/
    /**
     * 获取锁  如果锁可用   立即返回true，  否则返回false
     *
     * @return
     */
    public boolean tryLock(String basePrefix) {
        return tryLock(basePrefix, 0L, null);
    }


    /**
     * EX second
     * PX millisecond
     * NX ：只在键不存在时才对键进行设置操作
     * XX ：只在键已经存在时，才对键进行设置操作。
     * @param key
     * @param expire
     * @return
     */
    private boolean setRedisNXPX(final String key, final long expire) {
        String uuid = UUID.randomUUID().toString().replace("-", "");
        lockFlag.set(uuid);
        try {
//            String result = jedis.set(key, uuid, "NX", "PX", expire);
            boolean result = valueOps.setIfAbsent(key, uuid, expire, TimeUnit.MILLISECONDS);

            logger.info("lock key {} [{}] : ", key, result);
            return result;
        } catch (Exception e) {
            logger.error("set redis occured an exception", e);
            return false;
        }
    }

    public boolean lockByNXPX(String key, long expire) {
        return lockByNXPX(key, expire, 0, 0L);
    }

    /**
     * @param key
     * @param expire  锁的时间 ，毫秒数
     * @param retryTimes  重试次数
     * @param sleepMillis  重试间隔 毫秒数
     * @return
     */
    public boolean lockByNXPX(String key, long expire, int retryTimes, long sleepMillis) {
        boolean result = setRedisNXPX(key, expire);
        // 如果获取锁失败，按照传入的重试次数进行重试
        while (!Thread.currentThread().isInterrupted() && (!result) && retryTimes-- > 0) {
            try {
                Thread.sleep(sleepMillis);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                logger.error("获取Redis锁失败，将线程挂起。key: {} - expire: {} - retryTimes: {} - sleepMillis: {}", key, expire, retryTimes, sleepMillis);
                return false;
            }
            result = setRedisNXPX(key, expire);
        }
        return result;
    }

    public boolean releaseLockByNXPX(String key) {
        try {
            // 释放锁的时候，有可能因为持锁之后方法执行时间大于锁的有效期，此时有可能已经被另外一个线程持有锁，所以不能直接删除
            String val = get(key);
            if (!StringUtils.isEmpty(val) && val.equals(lockFlag.get())) {
                deleteKey(key);
                logger.info("release lock ：{} [{}]", key);
            }
            return true;
        } catch (Exception e) {
            logger.error("release lock occured an exception", e);
            return false;
        }
    }

    /**
     * 锁在给定的等待时间内空闲，则获取锁成功 返回true， 否则返回false
     *
     * @param basePrefix
     * @param timeout
     * @param unit
     * @return
     */
    public boolean tryLock(String basePrefix, long timeout, TimeUnit unit) {
        return tryLock(basePrefix, timeout, unit, timeout, unit);
    }

    /**
     * 锁定
     * @param basePrefix 锁定内容
     * @param lockTime 锁定时间
     * @param lockUnit
     * @param waitTime 等待时间
     * @param waitUnit
     * @return
     */
    public boolean tryLock(String basePrefix, long lockTime, TimeUnit lockUnit, long waitTime, TimeUnit waitUnit) {
        String key = basePrefix;
        try {
            long nano = System.nanoTime();
            do {
                logger.debug("try lock key: " + key);
                boolean result = valueOps.setIfAbsent(key, key, lockTime, lockUnit);
                if (result) {
                    logger.debug("get lock, key: " + key + " , expire in " + basePrefix + " seconds.");
                    return Boolean.TRUE;
                } else { // 存在锁
                    if (logger.isDebugEnabled()) {
                        String desc = get(key);
                        logger.debug("key: " + key + " locked by another business：" + desc);
                    }
                }
                if (waitTime == 0) {
                    break;
                }
                Thread.sleep(300);
            } while ((System.nanoTime() - nano) < waitUnit.toNanos(waitTime));
            return Boolean.FALSE;
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return Boolean.FALSE;
    }

    /**
     * 如果锁空闲立即返回   获取失败 一直等待、30秒后失效
     * @param key
     */
    public void lock(String key) {
        lock(key, 30);
    }

    /**
     * 如果锁空闲立即返回   获取失败 一直等待
     *
     * @param basePrefix
     * @param seconds 失效时间 秒
     */
    public void lock(String basePrefix, long seconds) {
        String key = basePrefix;
        try {
            do {
                logger.debug("lock key: " + key);
                boolean result = valueOps.setIfAbsent(key, key, seconds, TimeUnit.SECONDS);
                if (result) {
                    logger.debug("get lock, key: " + key + " , expire in " + seconds + " seconds.");
                    return;
                } else {
                    if (logger.isDebugEnabled()) {
                        String desc = get(key);
                        logger.debug("key: " + key + " locked by another business：" + desc);
                    }
                }
                Thread.sleep(300);
            } while (true);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    /**
     * 释放锁
     *
     * @param basePrefix
     */
    public void unLock(String basePrefix) {
        deleteKey(basePrefix);
    }

    /**
     * 批量释放锁
     *
     * @param billIdentifyList
     */
    public void unLock(List<String> billIdentifyList) {
        try {
            deleteKeys(billIdentifyList);
            logger.debug("release lock, keys :{}", billIdentifyList);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    /************************  redis 事务 *******************************/
    /**
     * 开启事务
     */
    public void multi() {
        redisTemplate.multi();
    }

    /**
     * 提交事务
     */
    public void exec() {
        redisTemplate.exec();
    }


    /************************  GEO *******************************/
    /**
     * 添加点
     * @param key 这一批对应的键
     * @param p 坐标
     * @param m 坐标对应名称
     * @return
     */
    public Long geoAdd(String key, Point p, String m) {
        return geoOps.add(key, p, m);
    }

    /**
     * 添加点
     * @param key 这一批对应的键
     * @param var2 坐标
     * @return
     */
    public Long geoAdd(String key, RedisGeoCommands.GeoLocation<String> var2) {
        return geoOps.add(key, var2);
    }

    /**
     * 添加点
     * @param key 这一批对应的键
     * @param points 多个坐标
     * @return
     */
    public Long geoAdd(String key, Map<String, Point> points) {
        return geoOps.add(key, points);
    }

    /**
     * 添加点
     * @param key 这一批对应的键
     * @param locations 坐标列表
     * @return
     */
    public Long geoAdd(String key, Iterable<RedisGeoCommands.GeoLocation<String>> locations) {
        return geoOps.add(key, locations);
    }

    /**
     * 计算距离
     * @param key 这一批对应的键
     * @param m1 坐标1名称
     * @param m2 坐标2名称
     * @return
     */
    public Distance geoDistance(String key, String m1, String m2) {
        return geoOps.distance(key, m1, m2);
    }

    /**
     * 计算距离
     * @param key 这一批对应的键
     * @param m1 坐标1名称
     * @param m2 坐标2名称
     * @param metric 距离单位
     * @return
     */
    public Distance geoDistance(String key, String m1, String m2, Metric metric) {
        return geoOps.distance(key, m1, m2, metric);
    }

    /**
     * 返回坐标哈唏值列表
     * @param key 这一批对应的键
     * @param ms 坐标
     * @return
     */
    public List<String> geoHash(String key, String... ms) {
        return geoOps.hash(key, ms);
    }

    /**
     * 返回坐标对应的经纬度
     * @param key 这一批对应的键
     * @param ms 坐标名称
     * @return
     */
    public List<Point> geoPosition(String key, String... ms) {
        return geoOps.position(key, ms);
    }

    /**
     * 返回某坐标半径内的坐标列表
     * @param key 这一批对应的键
     * @param circle 中心点
     * @return
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadius(String key, Circle circle) {
        return geoOps.radius(key, circle);
    }

    /**
     * 返回某坐标半径内的坐标列表
     * @param key 这一批对应的键
     * @param circle 中心点
     * @param args 返回结果处理条件
     * @return
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadius(String key, Circle circle, RedisGeoCommands.GeoRadiusCommandArgs args) {
        return geoOps.radius(key, circle, args);
    }

    /**
     * 删除某个坐标
     * @param key 这一批对应的键
     * @param ms 坐标名称
     * @return
     */
    public Long geoRemove(String key, String... ms) {
        return geoOps.remove(key, ms);
    }
}
